Merge "mw.loader: Fix regression that caused CSS load after scripts."
authorTrevor Parscal <tparscal@wikimedia.org>
Mon, 8 Apr 2013 16:52:49 +0000 (16:52 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 8 Apr 2013 16:52:49 +0000 (16:52 +0000)
1  2 
resources/mediawiki/mediawiki.js
tests/qunit/suites/resources/mediawiki/mediawiki.test.js

@@@ -32,8 -32,8 +32,8 @@@ var mw = ( function ( $, undefined ) 
                 *
                 * If called with no arguments, all values will be returned.
                 *
 -               * @param selection mixed String key or array of keys to get values for.
 -               * @param fallback mixed Value to use in case key(s) do not exist (optional).
 +               * @param {string|Array} selection String key or array of keys to get values for.
 +               * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
                 * @return mixed If selection was a string returns the value or null,
                 *  If selection was an array, returns an object of key/values (value is null if not found),
                 *  If selection was not passed or invalid, will return the 'values' object member (be careful as
@@@ -73,8 -73,8 +73,8 @@@
                /**
                 * Sets one or multiple key/value pairs.
                 *
 -               * @param selection {mixed} String key or array of keys to set values for.
 -               * @param value {mixed} Value to set (optional, only in use when key is a string)
 +               * @param {string|Object} selection String key to set value for, or object mapping keys to values.
 +               * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
                 * @return {Boolean} This returns true on success, false on failure.
                 */
                set: function ( selection, value ) {
@@@ -96,7 -96,7 +96,7 @@@
                /**
                 * Checks if one or multiple keys exist.
                 *
 -               * @param selection {mixed} String key or array of keys to check
 +               * @param {Mixed} selection String key or array of keys to check
                 * @return {boolean} Existence of key(s)
                 */
                exists: function ( selection ) {
  
        Message.prototype = {
                /**
 -               * Simple message parser, does $N replacement, HTML-escaping (only for
 -               * 'escaped' format), and nothing else.
 +               * Simple message parser, does $N replacement and nothing else.
                 *
                 * This may be overridden to provide a more complex message parser.
                 *
                         *             'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
                         *             'group': 'somegroup', (or) null,
                         *             'source': 'local', 'someforeignwiki', (or) null
-                        *             'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
+                        *             'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
                         *             'script': ...,
                         *             'style': ...,
                         *             'messages': { 'key': 'value' },
                                // Selector cache for the marker element. Use getMarker() to get/use the marker!
                                $marker = null,
                                // Buffer for addEmbeddedCSS.
-                               cssBuffer = '';
+                               cssBuffer = '',
+                               // Callbacks for addEmbeddedCSS.
+                               cssCallbacks = $.Callbacks();
  
                        /* Private methods */
  
                        /**
                         * @param {string} [cssText=cssBuffer] If called without cssText,
                         * the internal buffer will be inserted instead.
+                        * @param {Function} callback
                         */
-                       function addEmbeddedCSS( cssText ) {
+                       function addEmbeddedCSS( cssText, callback ) {
                                var $style, styleEl;
  
+                               if ( callback ) {
+                                       cssCallbacks.add( callback );
+                               }
                                // Yield once before inserting the <style> tag. There are likely
                                // more calls coming up which we can combine this way.
                                // Appending a stylesheet and waiting for the browser to repaint
                                                } else {
                                                        styleEl.appendChild( document.createTextNode( String( cssText ) ) );
                                                }
+                                               cssCallbacks.fire().empty();
                                                return;
                                        }
                                }
  
                                $( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
+                               cssCallbacks.fire().empty();
                        }
  
                        /**
                         *
                         * @private
                         * @param {string|string[]} states Module states to filter by
 -                       * @param {Array} modules List of module names to filter (optional, by default the entire
 +                       * @param {Array} [modules] List of module names to filter (optional, by default the entire
                         * registry is used)
                         * @return {Array} List of filtered module names
                         */
                         * @param {string} module Module name to execute
                         */
                        function execute( module ) {
-                               var key, value, media, i, urls, script, markModuleReady, nestedAddScript;
+                               var key, value, media, i, urls, cssHandle, checkCssHandles,
+                                       cssHandlesRegistered = false;
  
                                if ( registry[module] === undefined ) {
                                        throw new Error( 'Module has not been registered yet: ' + module );
                                } else if ( registry[module].state === 'loading' ) {
                                        throw new Error( 'Module has not completed loading yet: ' + module );
                                } else if ( registry[module].state === 'ready' ) {
-                                       throw new Error( 'Module has already been loaded: ' + module );
+                                       throw new Error( 'Module has already been executed: ' + module );
                                }
  
                                /**
                                        el.href = url;
                                }
  
+                               function runScript() {
+                                       var script, markModuleReady, nestedAddScript;
+                                       try {
+                                               script = registry[module].script;
+                                               markModuleReady = function () {
+                                                       registry[module].state = 'ready';
+                                                       handlePending( module );
+                                               };
+                                               nestedAddScript = function ( arr, callback, async, i ) {
+                                                       // Recursively call addScript() in its own callback
+                                                       // for each element of arr.
+                                                       if ( i >= arr.length ) {
+                                                               // We're at the end of the array
+                                                               callback();
+                                                               return;
+                                                       }
+                                                       addScript( arr[i], function () {
+                                                               nestedAddScript( arr, callback, async, i + 1 );
+                                                       }, async );
+                                               };
+                                               if ( $.isArray( script ) ) {
+                                                       nestedAddScript( script, markModuleReady, registry[module].async, 0 );
+                                               } else if ( $.isFunction( script ) ) {
+                                                       registry[module].state = 'ready';
+                                                       script( $ );
+                                                       handlePending( module );
+                                               }
+                                       } catch ( e ) {
+                                               // This needs to NOT use mw.log because these errors are common in production mode
+                                               // and not in debug mode, such as when a symbol that should be global isn't exported
+                                               log( 'Exception thrown by ' + module + ': ' + e.message, e );
+                                               registry[module].state = 'error';
+                                               handlePending( module );
+                                       }
+                               }
+                               // This used to be inside runScript, but since that is now fired asychronously
+                               // (after CSS is loaded) we need to set it here right away. It is crucial that
+                               // when execute() is called this is set synchronously, otherwise modules will get
+                               // executed multiple times as the registry will state that it isn't loading yet.
+                               registry[module].state = 'loading';
+                               // Add localizations to message system
+                               if ( $.isPlainObject( registry[module].messages ) ) {
+                                       mw.messages.set( registry[module].messages );
+                               }
+                               // Make sure we don't run the scripts until all (potentially asynchronous)
+                               // stylesheet insertions have completed.
+                               ( function () {
+                                       var pending = 0;
+                                       checkCssHandles = function () {
+                                               // cssHandlesRegistered ensures we don't take off too soon, e.g. when
+                                               // one of the cssHandles is fired while we're still creating more handles.
+                                               if ( cssHandlesRegistered && pending === 0 && runScript ) {
+                                                       runScript();
+                                                       runScript = undefined; // Revoke
+                                               }
+                                       };
+                                       cssHandle = function () {
+                                               var check = checkCssHandles;
+                                               pending++;
+                                               return function () {
+                                                       if (check) {
+                                                               pending--;
+                                                               check();
+                                                               check = undefined; // Revoke
+                                                       }
+                                               };
+                                       };
+                               }() );
                                // Process styles (see also mw.loader.implement)
                                // * back-compat: { <media>: css }
                                // * back-compat: { <media>: [url, ..] }
                                                                // Strings are pre-wrapped in "@media". The media-type was just ""
                                                                // (because it had to be set to something).
                                                                // This is one of the reasons why this format is no longer used.
-                                                               addEmbeddedCSS( value );
+                                                               addEmbeddedCSS( value, cssHandle() );
                                                        } else {
                                                                // back-compat: { <media>: [url, ..] }
                                                                media = key;
                                                                        addLink( media, value[i] );
                                                                } else if ( key === 'css' ) {
                                                                        // { "css": [css, ..] }
-                                                                       addEmbeddedCSS( value[i] );
+                                                                       addEmbeddedCSS( value[i], cssHandle() );
                                                                }
                                                        }
                                                // Not an array, but a regular object
                                        }
                                }
  
-                               // Add localizations to message system
-                               if ( $.isPlainObject( registry[module].messages ) ) {
-                                       mw.messages.set( registry[module].messages );
-                               }
-                               // Execute script
-                               try {
-                                       script = registry[module].script;
-                                       markModuleReady = function () {
-                                               registry[module].state = 'ready';
-                                               handlePending( module );
-                                       };
-                                       nestedAddScript = function ( arr, callback, async, i ) {
-                                               // Recursively call addScript() in its own callback
-                                               // for each element of arr.
-                                               if ( i >= arr.length ) {
-                                                       // We're at the end of the array
-                                                       callback();
-                                                       return;
-                                               }
-                                               addScript( arr[i], function () {
-                                                       nestedAddScript( arr, callback, async, i + 1 );
-                                               }, async );
-                                       };
-                                       if ( $.isArray( script ) ) {
-                                               registry[module].state = 'loading';
-                                               nestedAddScript( script, markModuleReady, registry[module].async, 0 );
-                                       } else if ( $.isFunction( script ) ) {
-                                               registry[module].state = 'ready';
-                                               script( $ );
-                                               handlePending( module );
-                                       }
-                               } catch ( e ) {
-                                       // This needs to NOT use mw.log because these errors are common in production mode
-                                       // and not in debug mode, such as when a symbol that should be global isn't exported
-                                       log( 'Exception thrown by ' + module + ': ' + e.message, e );
-                                       registry[module].state = 'error';
-                                       handlePending( module );
-                               }
+                               // Kick off.
+                               cssHandlesRegistered = true;
+                               checkCssHandles();
                        }
  
                        /**
                                 * Registers a module, letting the system know about it and its
                                 * properties. Startup modules contain calls to this function.
                                 *
 -                               * @param module {String}: Module name
 -                               * @param version {Number}: Module version number as a timestamp (falls backs to 0)
 -                               * @param dependencies {String|Array|Function}: One string or array of strings of module
 +                               * @param {string} module Module name
 +                               * @param {number} version Module version number as a timestamp (falls backs to 0)
 +                               * @param {string|Array|Function} dependencies One string or array of strings of module
                                 *  names on which this module depends, or a function that returns that array.
 -                               * @param group {String}: Group which the module is in (optional, defaults to null)
 -                               * @param source {String}: Name of the source. Defaults to local.
 +                               * @param {string} [group=null] Group which the module is in
 +                               * @param {string} [source='local'] Name of the source
                                 */
                                register: function ( module, version, dependencies, group, source ) {
                                        var m;
                                /**
                                 * Executes a function as soon as one or more required modules are ready
                                 *
 -                               * @param dependencies {String|Array} Module name or array of modules names the callback
 +                               * @param {string|Array} dependencies Module name or array of modules names the callback
                                 *  dependends on to be ready before executing
 -                               * @param ready {Function} callback to execute when all dependencies are ready (optional)
 -                               * @param error {Function} callback to execute when if dependencies have a errors (optional)
 +                               * @param {Function} [ready] callback to execute when all dependencies are ready
 +                               * @param {Function} [error] callback to execute when if dependencies have a errors
                                 */
                                using: function ( dependencies, ready, error ) {
                                        var tod = typeof dependencies;
                                /**
                                 * Loads an external script or one or more modules for future use
                                 *
 -                               * @param modules {mixed} Either the name of a module, array of modules,
 +                               * @param {string|Array} modules Either the name of a module, array of modules,
                                 *  or a URL of an external script or style
 -                               * @param type {String} mime-type to use if calling with a URL of an
 +                               * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
                                 *  external script or style; acceptable values are "text/css" and
                                 *  "text/javascript"; if no type is provided, text/javascript is assumed.
 -                               * @param async {Boolean} (optional) If true, load modules asynchronously
 -                               *  even if document ready has not yet occurred. If false (default),
 -                               *  block before document ready and load async after. If not set, true will
 -                               *  be assumed if loading a URL, and false will be assumed otherwise.
 +                               * @param {boolean} [async] If true, load modules asynchronously
 +                               *  even if document ready has not yet occurred. If false, block before
 +                               *  document ready and load async after. If not set, true will be
 +                               *  assumed if loading a URL, and false will be assumed otherwise.
                                 */
                                load: function ( modules, type, async ) {
                                        var filtered, m, module, l;
                                /**
                                 * Changes the state of a module
                                 *
 -                               * @param module {String|Object} module name or object of module name/state pairs
 -                               * @param state {String} state name
 +                               * @param {string|Object} module module name or object of module name/state pairs
 +                               * @param {string} state state name
                                 */
                                state: function ( module, state ) {
                                        var m;
                                /**
                                 * Gets the version of a module
                                 *
 -                               * @param module string name of module to get version for
 +                               * @param {string} module name of module to get version for
                                 */
                                getVersion: function ( module ) {
                                        if ( registry[module] !== undefined && registry[module].version !== undefined ) {
                                /**
                                 * Gets the state of a module
                                 *
 -                               * @param module string name of module to get state for
 +                               * @param {string} module name of module to get state for
                                 */
                                getState: function ( module ) {
                                        if ( registry[module] !== undefined && registry[module].state !== undefined ) {
                                /**
                                 * Create an HTML element string, with safe escaping.
                                 *
 -                               * @param name The tag name.
 -                               * @param attrs An object with members mapping element names to values
 -                               * @param contents The contents of the element. May be either:
 +                               * @param {string} name The tag name.
 +                               * @param {Object} attrs An object with members mapping element names to values
 +                               * @param {Mixed} contents The contents of the element. May be either:
                                 *  - string: The string is escaped.
                                 *  - null or undefined: The short closing form is used, e.g. <br/>.
                                 *  - this.Raw: The value attribute is included without escaping.
                                'gender-plural-msg': '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome',
                                'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
                                'formatnum-msg': '{{formatnum:$1}}',
 -                              'int-msg': 'Some {{int:other-message}}'
 +                              'int-msg': 'Some {{int:other-message}}',
 +                              'mediawiki-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
 +                              'external-link-replace': 'Foo [$1 bar]'
                        } );
  
 -                      // For formatnum tests
 -                      mw.config.set( 'wgUserLanguage', 'en' );
 +                      mw.config.set( {
 +                              wgArticlePath: '/wiki/$1',
 +
 +                              // For formatnum tests
 +                              wgUserLanguage: 'en'
 +                      } );
  
                        specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
                }
                assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' );
        } );
  
 -      QUnit.test( 'mw.message & mw.messages', 54, function ( assert ) {
 +      QUnit.test( 'mw.message & mw.messages', 68, function ( assert ) {
                var goodbye, hello;
  
                // Convenience method for asserting the same result for multiple formats
                assert.equal( hello.escaped(), 'Hello &lt;b&gt;awesome&lt;/b&gt; world', 'Message.escaped returns the escaped message' );
                assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
  
 -              assert.ok( mw.messages.set( 'escaped-with-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ) );
 -              assert.equal( mw.message( 'escaped-with-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName' ) + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
 +              assert.ok( mw.messages.set( 'multiple-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ), 'mw.messages.set: Register' );
 +              assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message', 'Curly brace format works correctly' );
 +              assert.equal( mw.message( 'multiple-curly-brace' ).plain(), mw.messages.get( 'multiple-curly-brace' ), 'Plain format works correctly for curly brace message' );
 +              assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
 +
 +              assert.ok( mw.messages.set( 'multiple-square-brackets-and-ampersand', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ), 'mw.messages.set: Register' );
 +              assertMultipleFormats( ['multiple-square-brackets-and-ampersand'], ['plain', 'text'], mw.messages.get( 'multiple-square-brackets-and-ampersand' ), 'Square bracket message is not processed' );
 +              assert.equal( mw.message( 'multiple-square-brackets-and-ampersand' ).escaped(), 'Visit the [[Project:Community portal|community portal]] &amp; [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
 +              assert.htmlEqual( mw.message( 'multiple-square-brackets-and-ampersand' ).parse(), 'Visit the ' +
 +                      '<a title="Project:Community portal" href="/wiki/Project:Community_portal">community portal</a>' +
 +                      ' &amp; <a title="Project:Help desk" href="/wiki/Project:Help_desk">help desk</a>', 'Internal links work with parse' );
 +
 +              assertMultipleFormats( ['mediawiki-test-version-entrypoints-index-php'], ['plain', 'text', 'escaped'], mw.messages.get( 'mediawiki-test-version-entrypoints-index-php' ), 'External link markup is unprocessed' );
 +              assert.htmlEqual( mw.message( 'mediawiki-test-version-entrypoints-index-php' ).parse(), '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>', 'External link works correctly in parse mode' );
  
 -              assert.ok( mw.messages.set( 'escaped-with-square-brackets', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ) );
 -              assert.equal( mw.message( 'escaped-with-square-brackets' ).escaped(), 'Visit the [[Project:Community portal|community portal]] &amp; [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
 +              assertMultipleFormats( ['external-link-replace', 'http://example.org/?x=y&z'], ['plain', 'text'] , 'Foo [http://example.org/?x=y&z bar]', 'Parameters are substituted but external link is not processed' );
 +              assert.equal( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).escaped(), 'Foo [http://example.org/?x=y&amp;z bar]', 'In escaped mode, parameters are substituted and ampersand is escaped, but external link is not processed' );
 +              assert.htmlEqual( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).parse(), 'Foo <a href="http://example.org/?x=y&amp;z">bar</a>', 'External link with replacement works in parse mode without double-escaping' );
  
                hello.parse();
                assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' );
                assert.ok( mw.messages.set( 'mediawiki-test-categorytree-collapse-bullet', '[<b>−</b>]' ), 'mw.messages.set: Register' );
                assert.equal( mw.message( 'mediawiki-test-categorytree-collapse-bullet' ).plain(), mw.messages.get( 'mediawiki-test-categorytree-collapse-bullet' ), 'Single square brackets unchanged in plain mode' );
  
 -              assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ) );
 +              assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ), 'mw.messages.set: Register' );
                assert.equal( mw.message( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ).plain(), mw.messages.get( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ), 'HTML message with curly braces is not changed in plain mode' );
  
                assertMultipleFormats( ['gender-plural-msg', 'male', 1], ['text', 'parse', 'escaped'], 'he is awesome', 'Gender and plural are resolved' );
                assert.equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' );
                assert.equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' );
  
 -              assert.ok( mw.messages.set( 'plural-item', 'Found $1 {{PLURAL:$1|item|items}}' ) );
 +              assert.ok( mw.messages.set( 'plural-item' , 'Found $1 {{PLURAL:$1|item|items}}' ), 'mw.messages.set: Register' );
                assert.equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' );
                assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' );
                assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' );
        function assertStyleAsync( assert, $element, prop, val, fn ) {
                var styleTestStart,
                        el = $element.get( 0 ),
-                       styleTestTimeout = ( QUnit.config.testTimeout - 200 ) || 5000;
+                       styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
  
                function isCssImportApplied() {
                        // Trigger reflow, repaint, redraw, whatever (cross-browser)
                } );
        } );
  
-       QUnit.test( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
+       QUnit.asyncTest( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
                var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
  
                assert.notEqual(
                mw.loader.implement(
                        'test.implement.a',
                        function () {
-                               QUnit.stop();
-                               setTimeout(function () {
-                                       assert.equal(
-                                               $element.css( 'float' ),
-                                               'right',
-                                               'style is applied'
-                                       );
-                                       QUnit.start();
-                               });
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'style is applied'
+                               );
+                               QUnit.start();
                        },
                        {
                                'all': '.mw-test-implement-a { float: right; }'
                ] );
        } );
  
- // Backwards compatibility
-       QUnit.test( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
      // Backwards compatibility
+       QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
                var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
  
                assert.notEqual(
                mw.loader.implement(
                        'test.implement.c',
                        function () {
-                               QUnit.stop();
-                               setTimeout(function () {
-                                       assert.equal(
-                                               $element.css( 'float' ),
-                                               'right',
-                                               'style is applied'
-                                       );
-                                       QUnit.start();
-                               });
+                               assert.equal(
+                                       $element.css( 'float' ),
+                                       'right',
+                                       'style is applied'
+                               );
+                               QUnit.start();
                        },
                        {
                                'all': '.mw-test-implement-c { float: right; }'
                ] );
        } );
  
- // Backwards compatibility
      // Backwards compatibility
        QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
                var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
                        $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' );
                ] );
        } );
  
- // @import (bug 31676)
      // @import (bug 31676)
        QUnit.asyncTest( 'mw.loader.implement( styles has @import)', 5, function ( assert ) {
                var isJsExecuted, $element;